package com.gitee.wsl.prism4j

object GrammarUtils {
    /**
     * Helper method to find a token inside grammar. Supports lookup in `inside` grammars. For
     * example given the path: `first-token/then-another/and-more ` this method will do:
     *
     *  * Look for `first-token` at root level of supplied grammar
     *  * If it\'s found search for first pattern with `inside` grammar
     *  * If it\'s found search for `then-another` token in this inside grammar
     *  * etc
     *
     * Simple path `simple-root-level ` is also supported
     *
     * @param grammar [Prism4j.Grammar]
     * @param path    argument to find a [Prism4j.Token]
     * @return a found [Prism4j.Token] or null
     */
    fun findToken(grammar: Prism4j.Grammar, path: String): Prism4j.Token? {
        val parts = path.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        return findToken(grammar, parts, 0)
    }

    private fun findToken(grammar: Prism4j.Grammar, parts: Array<String>, index: Int): Prism4j.Token? {
        val part = parts[index]
        val last = index == parts.size - 1

        for (token in grammar.tokens()) {
            if (part == token.name()) {
                if (last) {
                    return token
                } else {
                    val inside: Prism4j.Grammar? = findFirstInsideGrammar(token)
                    if (inside != null) {
                        return findToken(inside, parts, index + 1)
                    } else {
                        break
                    }
                }
            }
        }

        return null
    }

    // won't work if there are multiple patterns provided for a token (each with inside grammar)
    fun insertBeforeToken(
        grammar: Prism4j.Grammar,
        path: String,
        vararg tokens: Prism4j.Token
    ) {
        if (tokens.isEmpty()) {
            return
        }

        val parts = path.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

        insertBeforeToken(grammar = grammar, parts =  parts, index = 0, tokens = tokens.toList())
    }

    private fun insertBeforeToken(
        grammar: Prism4j.Grammar,
        parts: Array<String>,
        index: Int,
        tokens: List<Prism4j.Token>
    ) {
        val part = parts[index]
        val last = index == parts.size - 1

        val grammarTokens = grammar.tokens()

        var token: Prism4j.Token

        var i = 0
        val size = grammarTokens.size
        while (i < size) {
            token = grammarTokens[i]

            if (part == token.name()) {
                // here we must decide what to do next:
                //  - it can be out found one
                //  - or we need to go deeper (c)

                if (last) {
                    // here we go, it's our token
                    insertTokensAt(i, grammarTokens, tokens)
                } else {
                    // now we must find a grammar that is inside
                    // token can have multiple patterns
                    // but as they are not identified somehow (no name or anything)
                    // we will try to find first pattern with inside grammar
                    val inside: Prism4j.Grammar? = findFirstInsideGrammar(token)
                    if (inside != null) {
                        insertBeforeToken(inside, parts, index + 1, tokens)
                    }
                }

                // break after we have found token with specified name (most likely it won't repeat itself)
                break
            }
            i++
        }
    }

    fun findFirstInsideGrammar(token: Prism4j.Token): Prism4j.Grammar? {
        var grammar: Prism4j.Grammar? = null
        for (pattern in token.patterns()) {
            if (pattern.inside() != null) {
                grammar = pattern.inside()
                break
            }
        }
        return grammar
    }

    private fun insertTokensAt(
        start: Int,
        grammarTokens: MutableList<Prism4j.Token>,
        tokens: List<Prism4j.Token>
    ) {
        var i = 0
        val length = tokens.size
        while (i < length) {
            grammarTokens.add(start + i, tokens[i])
            i++
        }
    }

    fun clone(grammar: Prism4j.Grammar): Prism4j.Grammar {
        return CLONER.clone(grammar)
    }

    fun clone(token: Prism4j.Token): Prism4j.Token {
        return CLONER.clone(token)
    }

    fun clone(pattern: Prism4j.Pattern): Prism4j.Pattern {
        return CLONER.clone(pattern)
    }

    fun extend(
        grammar: Prism4j.Grammar,
        name: String,
        vararg tokens: Prism4j.Token
    ): Prism4j.Grammar {
        // we clone the whole grammar, but override top-most tokens that are passed here

        val size = tokens.size

        if (size == 0) {
            return GrammarImpl(name, clone(grammar).tokens())
        }

        val overrides: MutableMap<String?, Prism4j.Token?> = HashMap<String?, Prism4j.Token?>(size)
        for (token in tokens) {
            overrides.put(token.name(), token)
        }

        val origins = grammar.tokens()
        val out: MutableList<Prism4j.Token> = ArrayList(origins.size)

        var override: Prism4j.Token?

        for (origin in origins) {
            override = overrides[origin.name()]
            if (override != null) {
                out.add(override)
            } else {
                out.add(clone(origin))
            }
        }

        return GrammarImpl(name, out)
    }

    fun extend(
        grammar: Prism4j.Grammar,
        name: String,
        filter: TokenFilter,
        vararg tokens: Prism4j.Token
    ): Prism4j.Grammar {
        val size = tokens.size

        val overrides: MutableMap<String?, Prism4j.Token?>
        if (size == 0) {
            overrides = mutableMapOf<String?, Prism4j.Token?>()
        } else {
            overrides = HashMap<String?, Prism4j.Token?>(size)
            for (token in tokens) {
                overrides.put(token.name(), token)
            }
        }

        val origins: MutableList<Prism4j.Token> = grammar.tokens()
        val out: MutableList<Prism4j.Token> = ArrayList(origins.size)

        var override: Prism4j.Token?

        for (origin in origins) {
            // filter out undesired tokens

            if (!filter.test(origin)) {
                continue
            }

            override = overrides[origin.name()]
            if (override != null) {
                out.add(override)
            } else {
                out.add(clone(origin))
            }
        }

        return GrammarImpl(name, out)
    }

    fun require(prism4j: Prism4j, name: String): Prism4j.Grammar {
        val grammar = prism4j.grammar(name)
        checkNotNull(grammar) { "Unexpected state, requested language is not found: " + name }
        return grammar
    }

    private val CLONER: Cloner = Cloner.create()

    /**
     * Used when extending an existing grammar to filter out tokens that should not be cloned.
     *
     * @see .extend
     */
    fun interface TokenFilter {
        /**
         * @param token [Prism4j.Token] to validate
         * @return a boolean indicating if supplied token should be included (passes the test)
         */
        fun test(token: Prism4j.Token): Boolean
    }
}
